<?php

// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2010 http://topthink.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

namespace Think\Model;

use Think\Model;

/**
 * MongoModel模型类
 * 实现了ODM和ActiveRecords模式
 */
class MongoModel extends Model {

    // 主键类型
    const TYPE_OBJECT = 1;
    const TYPE_INT = 2;
    const TYPE_STRING = 3;

    // 主键名称
    protected $pk = '_id';
    // _id 类型 1 Object 采用MongoId对象 2 Int 整形 支持自动增长 3 String 字符串Hash
    protected $_idType = self::TYPE_OBJECT;
    // 主键是否自增
    protected $_autoinc = true;
    // Mongo默认关闭字段检测 可以动态追加字段
    protected $autoCheckFields = false;
    // 链操作方法列表
    protected $methods = array('table', 'order', 'auto', 'filter', 'validate');

    /**
     * 利用__call方法实现一些特殊的Model方法
     * @access public
     * @param string $method 方法名称
     * @param array $args 调用参数
     * @return mixed
     */
    public function __call($method, $args) {
        if (in_array(strtolower($method), $this->methods, true)) {
            // 连贯操作的实现
            $this->options[strtolower($method)] = $args[0];
            return $this;
        } elseif (strtolower(substr($method, 0, 5)) == 'getby') {
            // 根据某个字段获取记录
            $field = parse_name(substr($method, 5));
            $where[$field] = $args[0];
            return $this->where($where)->find();
        } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
            // 根据某个字段获取记录的某个值
            $name = parse_name(substr($method, 10));
            $where[$name] = $args[0];
            return $this->where($where)->getField($args[1]);
        } else {
            E(__CLASS__ . ':' . $method . L('_METHOD_NOT_EXIST_'));
            return;
        }
    }

    /**
     * 获取字段信息并缓存 主键和自增信息直接配置
     * @access public
     * @return void
     */
    public function flush() {
        // 缓存不存在则查询数据表信息
        $fields = $this->db->getFields();
        if (!$fields) { // 暂时没有数据无法获取字段信息 下次查询
            return false;
        }
        $this->fields = array_keys($fields);
        foreach ($fields as $key => $val) {
            // 记录字段类型
            $type[$key] = $val['type'];
        }
        // 记录字段类型信息
        if (C('DB_FIELDTYPE_CHECK'))
            $this->fields['_type'] = $type;

        // 2008-3-7 增加缓存开关控制
        if (C('DB_FIELDS_CACHE')) {
            // 永久缓存数据表信息
            $db = $this->dbName ? $this->dbName : C('DB_NAME');
            F('_fields/' . $db . '.' . $this->name, $this->fields);
        }
    }

    // 写入数据前的回调方法 包括新增和更新
    protected function _before_write(&$data) {
        $pk = $this->getPk();
        // 根据主键类型处理主键数据
        if (isset($data[$pk]) && $this->_idType == self::TYPE_OBJECT) {
            $data[$pk] = new \MongoId($data[$pk]);
        }
    }

    /**
     * count统计 配合where连贯操作
     * @access public
     * @return integer
     */
    public function count() {
        // 分析表达式
        $options = $this->_parseOptions();
        return $this->db->count($options);
    }

    /**
     * 获取下一ID 用于自动增长型
     * @access public
     * @param string $pk 字段名 默认为主键
     * @return mixed
     */
    public function getMongoNextId($pk = '') {
        if (empty($pk)) {
            $pk = $this->getPk();
        }
        return $this->db->mongo_next_id($pk);
    }

    /**
     * 新增数据
     * @access public
     * @param mixed $data 数据
     * @param array $options 表达式
     * @param boolean $replace 是否replace
     * @return mixed
     */
    public function add($data = '', $options = array(), $replace = false) {
        if (empty($data)) {
            // 没有传递数据，获取当前数据对象的值
            if (!empty($this->data)) {
                $data = $this->data;
                // 重置数据
                $this->data = array();
            } else {
                $this->error = L('_DATA_TYPE_INVALID_');
                return false;
            }
        }
        // 分析表达式
        $options = $this->_parseOptions($options);
        // 数据处理
        $data = $this->_facade($data);
        if (false === $this->_before_insert($data, $options)) {
            return false;
        }
        // 写入数据到数据库
        $result = $this->db->insert($data, $options, $replace);
        if (false !== $result) {
            $this->_after_insert($data, $options);
            if (isset($data[$this->getPk()])) {
                return $data[$this->getPk()];
            }
        }
        return $result;
    }

    // 插入数据前的回调方法
    protected function _before_insert(&$data, $options) {
        // 写入数据到数据库
        if ($this->_autoinc && $this->_idType == self::TYPE_INT) { // 主键自动增长
            $pk = $this->getPk();
            if (!isset($data[$pk])) {
                $data[$pk] = $this->db->mongo_next_id($pk);
            }
        }
    }

    public function clear() {
        return $this->db->clear();
    }

    // 查询成功后的回调方法
    protected function _after_select(&$resultSet, $options) {
        array_walk($resultSet, array($this, 'checkMongoId'));
    }

    /**
     * 获取MongoId
     * @access protected
     * @param array $result 返回数据
     * @return array
     */
    protected function checkMongoId(&$result) {
        if (is_object($result['_id'])) {
            $result['_id'] = $result['_id']->__toString();
        }
        return $result;
    }

    // 表达式过滤回调方法
    protected function _options_filter(&$options) {
        $id = $this->getPk();
        if (isset($options['where'][$id]) && is_scalar($options['where'][$id]) && $this->_idType == self::TYPE_OBJECT) {
            $options['where'][$id] = new \MongoId($options['where'][$id]);
        }
    }

    /**
     * 查询数据
     * @access public
     * @param mixed $options 表达式参数
     * @return mixed
     */
    public function find($options = array()) {
        if (is_numeric($options) || is_string($options)) {
            $id = $this->getPk();
            $where[$id] = $options;
            $options = array();
            $options['where'] = $where;
        }
        // 分析表达式
        $options = $this->_parseOptions($options);
        $result = $this->db->find($options);
        if (false === $result) {
            return false;
        }
        if (empty($result)) {// 查询结果为空
            return null;
        } else {
            $this->checkMongoId($result);
        }
        $this->data = $result;
        $this->_after_find($this->data, $options);
        return $this->data;
    }

    /**
     * 字段值增长
     * @access public
     * @param string $field  字段名
     * @param integer $step  增长值
     * @return boolean
     */
    public function setInc($field, $step = 1) {
        return $this->setField($field, array('inc', $step));
    }

    /**
     * 字段值减少
     * @access public
     * @param string $field  字段名
     * @param integer $step  减少值
     * @return boolean
     */
    public function setDec($field, $step = 1) {
        return $this->setField($field, array('inc', '-' . $step));
    }

    /**
     * 获取一条记录的某个字段值
     * @access public
     * @param string $field  字段名
     * @param string $spea  字段数据间隔符号
     * @return mixed
     */
    public function getField($field, $sepa = null) {
        $options['field'] = $field;
        $options = $this->_parseOptions($options);
        if (strpos($field, ',')) { // 多字段
            if (is_numeric($sepa)) {// 限定数量
                $options['limit'] = $sepa;
                $sepa = null; // 重置为null 返回数组
            }
            $resultSet = $this->db->select($options);
            if (!empty($resultSet)) {
                $_field = explode(',', $field);
                $field = array_keys($resultSet[0]);
                $key = array_shift($field);
                $key2 = array_shift($field);
                $cols = array();
                $count = count($_field);
                foreach ($resultSet as $result) {
                    $name = $result[$key];
                    if (2 == $count) {
                        $cols[$name] = $result[$key2];
                    } else {
                        $cols[$name] = is_null($sepa) ? $result : implode($sepa, $result);
                    }
                }
                return $cols;
            }
        } else {
            // 返回数据个数
            if (true !== $sepa) {// 当sepa指定为true的时候 返回所有数据
                $options['limit'] = is_numeric($sepa) ? $sepa : 1;
            }            // 查找一条记录
            $result = $this->db->find($options);
            if (!empty($result)) {
                if (1 == $options['limit'])
                    return reset($result[0]);
                foreach ($result as $val) {
                    $array[] = $val[$field];
                }
                return $array;
            }
        }
        return null;
    }

    /**
     * 执行Mongo指令
     * @access public
     * @param array $command  指令
     * @return mixed
     */
    public function command($command) {
        return $this->db->command($command);
    }

    /**
     * 执行MongoCode
     * @access public
     * @param string $code  MongoCode
     * @param array $args   参数
     * @return mixed
     */
    public function mongoCode($code, $args = array()) {
        return $this->db->execute($code, $args);
    }

    // 数据库切换后回调方法
    protected function _after_db() {
        // 切换Collection
        $this->db->switchCollection($this->getTableName(), $this->dbName ? $this->dbName : C('db_name'));
    }

    /**
     * 得到完整的数据表名 Mongo表名不带dbName
     * @access public
     * @return string
     */
    public function getTableName() {
        if (empty($this->trueTableName)) {
            $tableName = !empty($this->tablePrefix) ? $this->tablePrefix : '';
            if (!empty($this->tableName)) {
                $tableName .= $this->tableName;
            } else {
                $tableName .= parse_name($this->name);
            }
            $this->trueTableName = strtolower($tableName);
        }
        return $this->trueTableName;
    }

}
